package dungeonTools;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;
import be.kuleuven.cs.som.annotate.Value;


/**
 * A class of coordinates for mapping squares in 3D space.
 * The class is immutable. All coordinates with the same 
 * x, y and z values will evaluate to equal, and have the same
 * hashes.
 * 
 * Coordinates must all be positive.
 * 
 * @author Tom Jacobs & Ewoud De Moor
 * @version 3.0
 */
@Value
public final class Coordinate {
	
	/**
	 * Variable registering the x-coordinate
	 */
	private final long xCoordinate;
	
	/**
	 * Variable registering the y-coordinate
	 */
	private final long yCoordinate;

	/**
	 * Variable registering the z-coordinate
	 */
	private final long zCoordinate;
	
	/**
	 * Variable registering the hashcode of this Coordinate. Lazy initialization is used.
	 */
	private int hashcode = 0;
	
	/**
	 * Static variable registering the value at which hash-calculation is started
	 */
	private static final int HASH_INITIALIZING_VALUE = 0x7ffffff; //Chosen because 2^30 - 1 initializes every hash bit except the first 2 as being 1
	
	/**
	 * Static variable registering the value with which hash calculation multiplies for better uniform spreading
	 */
	private static final int HASH_MULTIPLICATION_VALUE = 524287; //Chosen as the 7th Mersenne prime, because we can, and also doesn't cause bit-shift
	
	/**
	 * Creates a new 3d Coordinate with coordinates (x,y,z)
	 * mapped by the parameters.
	 * 
	 * Coordinates must all be positive
	 * 
	 * @param xCoordinate
	 * 		The X-coordinate
	 * @param yCoordinate
	 * 		The Y-coordinate
	 * @param zCoordinate
	 * 		The Z-coordinate
	 * @throws IllegalArgumentException
	 * 		The coordinate values must be positive
	 * 		|(xCoordinate < 0) || (yCoordinate < 0) || (zCoordinate < 0 )
	 */
	@Raw
	public Coordinate(long xCoordinate, long yCoordinate, long zCoordinate) throws IllegalArgumentException{
		if((xCoordinate < 0) || (yCoordinate < 0) || (zCoordinate < 0 ))
			throw new IllegalArgumentException("Negative Coordinate Value");
		this.xCoordinate = xCoordinate;
		this.yCoordinate = yCoordinate;
		this.zCoordinate = zCoordinate;
	}

	/**
	 * Returns the X-coordinate of this Coordinate
	 * @return
	 * 		The X-coordinate
	 */
	@Basic @Immutable
	public final long getxCoordinate() {
		return xCoordinate;
	}
	
	/**
	 * Returns the Y-coordinate of this Coordinate
	 * @return
	 * 		The Y-coordinate
	 */
	@Basic @Immutable
	public final long getyCoordinate() {
		return yCoordinate;
	}

	/**
	 * Returns the Z-coordinate of this Coordinate
	 * @return
	 * 		The Z-coordinate
	 */
	@Basic @Immutable
	public final long getzCoordinate() {
		return zCoordinate;
	}

	@Override
	public boolean equals(Object obj) {
		if((obj != null) && (obj.getClass() == this.getClass())){
			Coordinate objAsCoordinate = ((Coordinate) obj);
			return ((this.getxCoordinate() == objAsCoordinate.getxCoordinate()) 
					&& (this.getyCoordinate() == objAsCoordinate.getyCoordinate()) 
					&& (this.getzCoordinate() == objAsCoordinate.getzCoordinate()));
		}
		return false;
	}

	@Override
	public int hashCode() {
		if(hashcode == 0){
			int hash = HASH_INITIALIZING_VALUE;
			for(int i = 0; i < 31 ; i++){
				hash = HASH_MULTIPLICATION_VALUE * hash + ((int) (getxCoordinate() ^ (getxCoordinate() >>> 32)));
				hash = HASH_MULTIPLICATION_VALUE * hash + ((int) (getyCoordinate() ^ (getyCoordinate() >>> 32)));
				hash = HASH_MULTIPLICATION_VALUE * hash + ((int) (getzCoordinate() ^ (getzCoordinate() >>> 32)));
				hash += hash >> 2;
				hash += hash >> 3;
				hash += hash >> 5;
				hash += hash >> 7;
				hash += hash << 2;
				hash += hash << 3;
				hash += hash << 5;
				hash += hash << 7;
			}
			this.hashcode = hash;
		}
		return hashcode;
	}

	@Override
	public String toString() {
		return "(" + getxCoordinate() + "," + getyCoordinate() + "," + getzCoordinate() + ")";
	}
	
	/**
	 * Gets the next coordinate in a given direction
	 * 
	 * @param direction
	 * 			The direction in which to get the next coordinate
	 * @return	The new coordinate in the given direction
	 * 			|switch(direction){ 
	 * 			|	case CEILING:
	 * 			|		result == new Coordinate(getxCoordinate(), getyCoordinate(), getzCoordinate() + 1);
	 * 			|	case EAST:
	 * 			|		result == new Coordinate(getxCoordinate() + 1, getyCoordinate(), getzCoordinate()); 
	 * 			|	case FLOOR:
	 * 			|		result == new Coordinate(getxCoordinate(), getyCoordinate(), getzCoordinate() - 1);
	 * 			|	case NORTH:
	 * 			|		result == new Coordinate(getxCoordinate(), getyCoordinate() + 1, getzCoordinate());
	 * 			|	case SOUTH:
	 * 			|		result == new Coordinate(getxCoordinate(), getyCoordinate() - 1, getzCoordinate());
	 * 			|	case WEST:
	 * 			|		result == new Coordinate(getxCoordinate() - 1, getyCoordinate(), getzCoordinate());
	 *			|}
	 * @throws IllegalArgumentException
	 * 			The direction must be initialized
	 * 			|direction == null
	 */
	public Coordinate getNextCoordinateInDirection(Direction direction) throws IllegalArgumentException{
		if(direction == null)
			throw new IllegalArgumentException("Please initialize direction");
		switch(direction){
		case CEILING:
			return new Coordinate(getxCoordinate(), getyCoordinate(), getzCoordinate() + 1);
		case EAST:
			return new Coordinate(getxCoordinate() + 1, getyCoordinate(), getzCoordinate()); 
		case FLOOR:
			return new Coordinate(getxCoordinate(), getyCoordinate(), getzCoordinate() - 1);
		case NORTH:
			return new Coordinate(getxCoordinate(), getyCoordinate() + 1, getzCoordinate());
		case SOUTH:
			return new Coordinate(getxCoordinate(), getyCoordinate() - 1, getzCoordinate());
		case WEST:
			return new Coordinate(getxCoordinate() - 1, getyCoordinate(), getzCoordinate());
		}
		return null;
	}

	
	/**
	 * Returns whether or not a certain coordinate has a LOWER value than this one, in a specific direction
	 * 
	 * @param coordinate
	 * 			The coordinate to compare to this one
	 * @param direction
	 * 			The direction in which to compare said coordinates
	 * @return	Whether or not said coordinate has a lower value than this one in said direction
	 * 			|switch(direction){
	 * 			|case FLOOR:
	 * 			|	result == getzCoordinate() < coordinate.getzCoordinate()
	 * 			|case SOUTH:
	 * 			|	result == getyCoordinate() < coordinate.getyCoordinate()
	 * 			|case WEST:
	 * 			|	result == getxCoordinate() < coordinate.getxCoordinate()
	 * 			|}
	 * @throws IllegalArgumentException
	 * 			The direction can't be null
	 * 			|direction == null
	 * 			The direction has to be a 'lower' direction
	 * 			|switch(direction){
	 * 			|case EAST
	 * 			|case CEILING
	 * 			|case WEST
	 * 			|}
	 */
	public boolean isLowerThan(Coordinate coordinate, Direction direction) throws IllegalArgumentException{
		if(direction == null)
			throw new IllegalArgumentException("Please initialize direction");
		switch(direction){
		case CEILING:
			throw new IllegalArgumentException("Not a valid direction");
		case EAST:
			throw new IllegalArgumentException("Not a valid direction");
		case FLOOR:
			return getzCoordinate() < coordinate.getzCoordinate();
		case NORTH:
			throw new IllegalArgumentException("Not a valid direction");
		case SOUTH:
			return getyCoordinate() < coordinate.getyCoordinate();
		case WEST:
			return getxCoordinate() < coordinate.getxCoordinate();
		}
		return false;
	}
	
	/**
	 * Variable referencing the origin (0,0,0). 
	 */
	public static final Coordinate ORIGIN = new Coordinate(0,0,0);
	
}
